EKS オーケストレータを使った SageMaker HyperPod クラスターで FSx for Lustre をマウントしてみた

EKS オーケストレータを使った SageMaker HyperPod クラスターで FSx for Lustre をマウントしてみた

Clock Icon2025.01.05

こんにちは!クラウド事業本部コンサルティング部のたかくに(@takakuni_)です。

みなさん SageMaker HyperPod 使っていますか?

今回は EKS オーケストレータを利用した SageMaker HyperPod で FSx for Lustre ファイルシステムをマウントする機会があったため、ブログにしてみたいと思います。

Slurm オーケストレータ版を探している場合は以下をご覧ください。

https://dev.classmethod.jp/articles/sagemaker-hyperpod-fsx-lustre-mount/

FSx for Lustre

AWS が提供する高性能なフルマネージドな分散並列ファイルシステムです。

FSx for Lustre を利用することで 1/100 秒未満のレイテンシ、最大数百 GBps のスループット、最大数百万の IOPS の性能を有したファイルシステムを利用できます。

データリポジトリ

FSx for Lustre では S3 と統合しており、 S3 オブジェクトをファイルとして透過的に書き込み/読み込みできます。

学習や推論で利用されるデータはサイズが大きいことが多々あり、各ノードが起動するタイミングで直接 S3 からダウンロードするのは時間もお金も大きくかかります。

FSx for Lustre を中継させることで、コスト効率を高める効果が見込めます。またチェックポイントを共有するファイルストレージとしても非常に有用です。

Untitled(114).png

通信要件

EKS オーケストレータの場合、 EKS クラスターセキュリティグループと HyperPod ノードで利用するセキュリティグループを意識する必要がありますが、 Lustre ファイルシステムと疎通は HyperPod ノードのセキュリティグループのみ意識しておけば OK です。具体的な通信要件は EFA の有無で異なります。

EFA 無し

EFA なしの場合は SageMaker HyperPod ノードおよびノード自身のセキュリティグループを 988, 1018-1023 ポートで許可するように設定します。

Lustre ファイルシステム側

インバウンドルール

タイプ プロトコル ポート ソース
Custom TCP TCP 988 自身のセキュリティグループ ID
Custom TCP TCP 988 SageMaker HyperPod ノードのセキュリティグループ ID
Custom TCP TCP 1018-1023 自身のセキュリティグループ ID
Custom TCP TCP 1018-1023 SageMaker HyperPod ノードのセキュリティグループ ID

アウトバウンドルール

タイプ プロトコル ポート ソース
Custom TCP TCP 988 自身のセキュリティグループ ID
Custom TCP TCP 988 SageMaker HyperPod ノードのセキュリティグループ ID
Custom TCP TCP 1018-1023 自身のセキュリティグループ ID
Custom TCP TCP 1018-1023 SageMaker HyperPod ノードのセキュリティグループ ID

Lustre クライアント側

インバウンドルール

タイプ プロトコル ポート ソース
Custom TCP TCP 988 自身のセキュリティグループ ID
Custom TCP TCP 988 Lustre ファイルシステムのセキュリティグループ ID
Custom TCP TCP 1018-1023 自身のセキュリティグループ ID
Custom TCP TCP 1018-1023 Lustre ファイルシステムのセキュリティグループ ID

アウトバウンドルール

タイプ プロトコル ポート ソース
Custom TCP TCP 988 自身のセキュリティグループ ID
Custom TCP TCP 988 Lustre ファイルシステムのセキュリティグループ ID
Custom TCP TCP 1018-1023 自身のセキュリティグループ ID
Custom TCP TCP 1018-1023 Lustre ファイルシステムのセキュリティグループ ID
すべてのトラフィック すべて すべて 0.0.0.0/0

※一番最後のすべてのトラフィックは外部接続用のルールのため、 Lustre とのやりとりとは別のルールです。

https://docs.aws.amazon.com/fsx/latest/LustreGuide/limit-access-security-groups.html#lustre-client-inbound-outbound-rules

EFA 有り

EFA 有りの場合は、すべての送受信トラフィックを許可する必要があります。

If you are going to create an EFA-enabled FSx for Lustre, you should first create an EFA-enabled security group and specify it as the security group for the file system. An EFA requires a security group that allows all inbound and outbound traffic to and from the security group itself and the security group of the clients if clients reside in a different security group. For more information, see Step 1: Prepare an EFA-enabled security group in the Amazon EC2 User Guide.

https://docs.aws.amazon.com/fsx/latest/LustreGuide/limit-access-security-groups.html#fsx-vpc-security-groups

要約すると以下のルールを追加する必要があります。

Lustre ファイルシステム側

インバウンドルール

タイプ プロトコル ポート ソース
すべてのトラフィック すべて すべて 自身のセキュリティグループ ID
すべてのトラフィック すべて すべて SageMaker HyperPod ノードのセキュリティグループ ID

アウトバウンドルール

タイプ プロトコル ポート ソース
すべてのトラフィック すべて すべて 自身のセキュリティグループ ID
すべてのトラフィック すべて すべて SageMaker HyperPod ノードのセキュリティグループ ID

Lustre クライアント側

インバウンドルール

タイプ プロトコル ポート ソース
すべてのトラフィック すべて すべて 自身のセキュリティグループ ID
すべてのトラフィック すべて すべて Lustre ファイルシステムのセキュリティグループ ID

アウトバウンドルール

タイプ プロトコル ポート ソース
すべてのトラフィック すべて すべて 自身のセキュリティグループ ID
すべてのトラフィック すべて すべて Lustre ファイルシステムのセキュリティグループ ID
すべてのトラフィック すべて すべて 0.0.0.0/0

一番最後のすべてのトラフィックは外部接続用のルールのため、 Lustre とのやりとりとは別のルールです。

0.0.0.0/0 で許可をしていますが、合わせてセキュリティグループ ID も許可してあげる必要があります。

If you want to create a HyperPod cluster with EFA-enabled instances, make sure that you set up a security group to allow all inbound and outbound traffic to and from the security group itself. Note that allowing outbound traffic to 0.0.0.0/0 isn't sufficient and can cause EFA health checks to fail. Make sure that you add an explicit outbound traffic rule to the security group so that the instances in the security group can communicate. To learn more, see Step 1: Prepare an EFA-enabled security group in the Amazon EC2 User Guide.

https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-prerequisites.html#sagemaker-hyperpod-prerequisites-optional-vpc

https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/efa-start.html#efa-start-security

SageMaker HyperPod 上で動かす際の注意点

FSx for Lustre は EKS 上の Pod に直接マウントはできないため、 Amazon FSx for Lustre CSI Driver を利用したボリュームのプロビジョニングが必要になります。

https://github.com/kubernetes-sigs/aws-fsx-csi-driver

Amazon FSx for Lustre CSI Driver では自動的に Lustre ファイルシステムから永続ボリューム要求(Persistent Volume Claim)まで作成してくれる Dynamic Provisioning と、手動で作成する Static Provisioning があります。

今回は具体的にイメージがつきやすい Static Provisioning を行います。

やってみる

今回は再現性があるよう HashiCorp Terraform を利用して作成しました。各ステップで何をどのように作成したのか触れていきたいと思います。

一連のコードは以下に格納しています。

https://github.com/takakuni-classmethod/genai-blog/tree/main/sagemaker_hyperpod_lustre_eks

基本的な EKS オーケストレータと SageMaker HyperPod の部分は省略します。(以下のエントリに記載しているので興味があれば合わせてご覧ください。)

https://dev.classmethod.jp/articles/creating-minimal-hyperpod-cluster-with-eks-orchestrator/

再掲になりますが、構成図は次の通りで EKS オーケストレータで動かす HyperPod クラスターがあり、インスタンスグループ(worker-group)の Pod に FSx for Lustre をマウントするような構成にしてみます。

Untitled(114).png

Lustre ファイルシステム

まずはファイルシステムの準備です。S3 に透過的にアクセスしたいため、aws_fsx_data_repository_association を利用してデータリポジトリを作成します。

lustre.tf(抜粋)
###################################################
# Lustre File System
###################################################
resource "aws_fsx_lustre_file_system" "this" {
  storage_type                = "SSD"
  file_system_type_version    = "2.15"
  storage_capacity            = 1200
  security_group_ids          = [aws_security_group.lustre.id]
  subnet_ids                  = [module.vpc.private_subnets[0]]
  data_compression_type       = "LZ4"
  deployment_type             = "PERSISTENT_2"
  per_unit_storage_throughput = 250

  metadata_configuration {
    mode = "AUTOMATIC"
  }
}

resource "aws_fsx_data_repository_association" "this" {
  file_system_id       = aws_fsx_lustre_file_system.this.id
  data_repository_path = "s3://${aws_s3_bucket.data_repository.bucket}"
  file_system_path     = "/"

  s3 {
    auto_export_policy {
      events = ["NEW", "CHANGED", "DELETED"]
    }

    auto_import_policy {
      events = ["NEW", "CHANGED", "DELETED"]
    }
  }
}

EFA を利用しないですがセキュリティグループは Lustre, HyperPod Node どちらもフルオープンで許可を行いました。

lustre.tf(抜粋)
###################################################
# Security Group for Lustre File System
###################################################
resource "aws_security_group" "lustre" {
  name        = "${local.prefix}-lustre-sg"
  vpc_id      = module.vpc.vpc_id
  description = "${local.prefix}-hyperpod-sg"

  tags = {
    Name = "${local.prefix}-lustre-sg"
  }
}
# Ingress
resource "aws_vpc_security_group_ingress_rule" "lustre_all_traffic_self" {
  security_group_id            = aws_security_group.lustre.id
  referenced_security_group_id = aws_security_group.lustre.id
  ip_protocol                  = "-1"
}

resource "aws_vpc_security_group_ingress_rule" "lustre_all_traffic_hyperpod" {
  security_group_id            = aws_security_group.lustre.id
  referenced_security_group_id = aws_security_group.hyperpod.id
  ip_protocol                  = "-1"
}

# Egress
resource "aws_vpc_security_group_egress_rule" "lustre_all_traffic_self" {
  security_group_id            = aws_security_group.lustre.id
  referenced_security_group_id = aws_security_group.lustre.id
  ip_protocol                  = "-1"
}

resource "aws_vpc_security_group_egress_rule" "lustre_all_traffic_hyperpod" {
  security_group_id            = aws_security_group.lustre.id
  referenced_security_group_id = aws_security_group.hyperpod.id
  ip_protocol                  = "-1"
}
hyperpod.tf(抜粋)
###################################################
# Security Group for SageMaker HyperPod Cluster
###################################################
resource "aws_security_group" "hyperpod" {
  name        = "${local.prefix}-hyperpod-sg"
  vpc_id      = module.vpc.vpc_id
  description = "${local.prefix}-hyperpod-sg"

  tags = {
    Name = "${local.prefix}-hyperpod-sg"
  }
}

resource "aws_vpc_security_group_ingress_rule" "hyperpod_allow_all_traffic_ipv4" {
  security_group_id            = aws_security_group.hyperpod.id
  referenced_security_group_id = aws_security_group.hyperpod.id
  ip_protocol                  = "-1"
}

resource "aws_vpc_security_group_ingress_rule" "hyperpod_allow_all_traffic_eks" {
  security_group_id            = aws_security_group.hyperpod.id
  referenced_security_group_id = aws_security_group.eks.id
  ip_protocol                  = "-1"
}

resource "aws_vpc_security_group_egress_rule" "hyperpod_allow_all_traffic_ipv4" {
  security_group_id = aws_security_group.hyperpod.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}

resource "aws_vpc_security_group_egress_rule" "hyperpod_allow_all_traffic_self" {
  security_group_id            = aws_security_group.hyperpod.id
  referenced_security_group_id = aws_security_group.hyperpod.id
  ip_protocol                  = "-1"
}

Amazon FSx for Lustre CSI Driver

続いて Amazon FSx for Lustre CSI Driver のセットアップです。

Amazon FSx for Lustre CSI Driver は IRSA の仕組みで認証認可を行うため、OpenID Connect を利用した IAM ロールを作成します。

lustre.tf(抜粋)
###################################################
# FSx CSI Driver
###################################################
data "tls_certificate" "this" {
  url = aws_eks_cluster.this.identity[0].oidc[0].issuer
  depends_on = [
    aws_eks_cluster.this
  ]
}

resource "aws_iam_openid_connect_provider" "this" {
  url = aws_eks_cluster.this.identity[0].oidc[0].issuer

  client_id_list = [
    "sts.amazonaws.com",
  ]

  thumbprint_list = [
    data.tls_certificate.this.certificates[0].sha1_fingerprint
  ]
}

data "aws_iam_policy_document" "assume_csi_driver" {
  statement {
    actions = [
      "sts:AssumeRoleWithWebIdentity",
    ]

    effect = "Allow"

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.this.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "${aws_iam_openid_connect_provider.this.url}:aud"
      values = [
        "sts.amazonaws.com",
      ]
    }
    condition {
      test     = "StringEquals"
      variable = "${aws_iam_openid_connect_provider.this.url}:sub"
      values = [
        "system:serviceaccount:kube-system:fsx-csi-controller-sa",
      ]
    }
  }
}

resource "aws_iam_role" "fsx_csi_driver" {
  name = "${local.prefix}-fsx-csi-driver-role"

  assume_role_policy = data.aws_iam_policy_document.assume_csi_driver.json
}

resource "aws_iam_role_policy_attachment" "fsx_csi_driver" {
  role       = aws_iam_role.fsx_csi_driver.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonFSxFullAccess"
}

OpenID Connect を利用した IAM ロールのセットアップが済んだら、Amazon FSx for Lustre CSI Driver を helm 経由でインストールします。

インストール後、helm チャートでデプロイしたサービスアカウント(fsx-csi-controller-sa)のアノテーションに先ほど作成した IAM ロールを紐付けます。

lustre.tf(抜粋)
resource "helm_release" "aws_fsx_csi_driver" {
  name              = "aws-fsx-csi-driver"
  repository        = "https://kubernetes-sigs.github.io/aws-fsx-csi-driver"
  chart             = "aws-fsx-csi-driver"
  namespace         = "kube-system"
  dependency_update = true
  wait              = false

  depends_on = [
    aws_eks_access_policy_association.this
  ]
}

data "kubernetes_service_account_v1" "this" {
  metadata {
    name      = "fsx-csi-controller-sa"
    namespace = "kube-system"
  }

  depends_on = [
    helm_release.aws_fsx_csi_driver,
    aws_eks_access_policy_association.this
  ]
}

resource "kubernetes_annotations" "this" {
  api_version = "v1"
  kind        = "ServiceAccount"

  metadata {
    name      = data.kubernetes_service_account_v1.this.metadata[0].name
    namespace = data.kubernetes_service_account_v1.this.metadata[0].namespace
  }

  annotations = {
    "eks.amazonaws.com/role-arn" = aws_iam_role.fsx_csi_driver.arn
  }

  depends_on = [
    aws_eks_access_policy_association.this
  ]
}

Persistent Volume

最後に Stroage Class, Persistent Volume, Persistent Volume Claim を作成します。これで Kubernetes 側の設定は完了です。

lustre.tf(抜粋)
###################################################
# FSx CSI Driver
###################################################
data "tls_certificate" "this" {
  url = aws_eks_cluster.this.identity[0].oidc[0].issuer
  depends_on = [
    aws_eks_cluster.this
  ]
}

resource "aws_iam_openid_connect_provider" "this" {
  url = aws_eks_cluster.this.identity[0].oidc[0].issuer

  client_id_list = [
    "sts.amazonaws.com",
  ]

  thumbprint_list = [
    data.tls_certificate.this.certificates[0].sha1_fingerprint
  ]
}

data "aws_iam_policy_document" "assume_csi_driver" {
  statement {
    actions = [
      "sts:AssumeRoleWithWebIdentity",
    ]

    effect = "Allow"

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.this.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "${aws_iam_openid_connect_provider.this.url}:aud"
      values = [
        "sts.amazonaws.com",
      ]
    }
    condition {
      test     = "StringEquals"
      variable = "${aws_iam_openid_connect_provider.this.url}:sub"
      values = [
        "system:serviceaccount:kube-system:fsx-csi-controller-sa",
      ]
    }
  }
}

resource "aws_iam_role" "fsx_csi_driver" {
  name = "${local.prefix}-fsx-csi-driver-role"

  assume_role_policy = data.aws_iam_policy_document.assume_csi_driver.json
}

resource "aws_iam_role_policy_attachment" "fsx_csi_driver" {
  role       = aws_iam_role.fsx_csi_driver.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonFSxFullAccess"
}

resource "helm_release" "aws_fsx_csi_driver" {
  name              = "aws-fsx-csi-driver"
  repository        = "https://kubernetes-sigs.github.io/aws-fsx-csi-driver"
  chart             = "aws-fsx-csi-driver"
  namespace         = "kube-system"
  dependency_update = true
  wait              = false

  depends_on = [
    aws_eks_access_policy_association.this
  ]
}

data "kubernetes_service_account_v1" "this" {
  metadata {
    name      = "fsx-csi-controller-sa"
    namespace = "kube-system"
  }

  depends_on = [
    helm_release.aws_fsx_csi_driver,
    aws_eks_access_policy_association.this
  ]
}

resource "kubernetes_annotations" "this" {
  api_version = "v1"
  kind        = "ServiceAccount"

  metadata {
    name      = data.kubernetes_service_account_v1.this.metadata[0].name
    namespace = data.kubernetes_service_account_v1.this.metadata[0].namespace
  }

  annotations = {
    "eks.amazonaws.com/role-arn" = aws_iam_role.fsx_csi_driver.arn
  }

  depends_on = [
    aws_eks_access_policy_association.this
  ]
}

###################################################
# Persistent Volume and Persistent Volume Claim
###################################################
resource "kubernetes_storage_class_v1" "this" {
  metadata {
    name = "fsx-sc"
  }
  storage_provisioner = "fsx.csi.aws.com"
  parameters = {
    "fileSystemId"     = aws_fsx_lustre_file_system.this.id
    "subnetId"         = aws_fsx_lustre_file_system.this.subnet_ids[0]
    "securityGroupIds" = aws_security_group.lustre.id
  }

  depends_on = [
    aws_eks_access_policy_association.this
  ]
}

resource "kubernetes_persistent_volume_v1" "this" {
  metadata {
    name = "fsx-pv"
  }

  spec {
    capacity = {
      storage = "1200Gi"
    }
    access_modes                     = ["ReadWriteMany"]
    volume_mode                      = "Filesystem"
    persistent_volume_reclaim_policy = "Retain"
    storage_class_name               = kubernetes_storage_class_v1.this.metadata[0].name
    persistent_volume_source {
      csi {
        driver        = "fsx.csi.aws.com"
        volume_handle = aws_fsx_lustre_file_system.this.id
        volume_attributes = {
          dnsname   = aws_fsx_lustre_file_system.this.dns_name
          mountname = aws_fsx_lustre_file_system.this.mount_name
        }
      }
    }
  }

  depends_on = [
    aws_eks_access_policy_association.this
  ]
}

resource "kubernetes_persistent_volume_claim_v1" "this" {
  metadata {
    name = "fsx-claim"
  }
  spec {
    access_modes       = ["ReadWriteMany"]
    storage_class_name = kubernetes_storage_class_v1.this.metadata[0].name
    resources {
      requests = {
        storage = "1200Gi"
      }
    }
  }

  depends_on = [
    aws_eks_access_policy_association.this,
    kubernetes_persistent_volume_v1.this
  ]
}

補足

PV の設定ですが Workshop の内容だと volumeAttributes の dnsnamemountname が指定されていません。

workshop の内容
cat <<EOF> pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: fsx-pv
spec:
  capacity:
    storage: 1200Gi  # Adjust based on your FSx volume size
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: fsx-sc
  csi:
    driver: fsx.csi.aws.com
    volumeHandle: fs-xxxxx  # Replace with your FSx file system ID
EOF

kubectl apply -f pv.yaml

https://catalog.workshops.aws/sagemaker-hyperpod-eks/en-US/01-cluster/06-fsx-for-lustre

この状態だとマウントできず、次のエラーが発生します。

MountVolume.SetUp failed for volume "fsx-pv" : rpc error: code = InvalidArgument desc = dnsname is not provided

そのため、 aws-fsx-csi-driver の例を参考に dnsnamemountname を指定しました。

https://github.com/kubernetes-sigs/aws-fsx-csi-driver/blob/master/examples/kubernetes/static_provisioning/specs/pv.yaml

動作確認

動作確認用にサンプルファイルを S3 にアップロードします。

lustre.tf(抜粋)
###################################################
# Hello Sample file
###################################################
resource "aws_s3_object" "hello_from_s3" {
  bucket = aws_s3_bucket.data_repository.bucket
  key    = "hello_from_s3.txt"

  content = "hello! from S3!"

  depends_on = [aws_fsx_data_repository_association.this]
}

また、先ほど作成したボリュームをマウントするように Pod を作成します。Pod からも Lustre にファイルを書き込むような動作を行います。

app.py
############################################
# Sample Kubernetes Pod
############################################
resource "kubernetes_pod_v1" "this" {
  metadata {
    name = "fsx-app"
  }
  spec {
    container {
      name    = "app"
      image   = "centos"
      command = ["/bin/sh"]
      args    = ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
      volume_mount {
        name       = "persistent-storage"
        mount_path = "/data"
      }
    }
    volume {
      name = "persistent-storage"
      persistent_volume_claim {
        claim_name = kubernetes_persistent_volume_claim_v1.this.metadata[0].name
      }
    }
  }

  depends_on = [
    kubernetes_persistent_volume_v1.this,
    awscc_sagemaker_cluster.this
  ]
}

Pod が作成後、 kubectl exec で Pod 内に bash ログインしてみます。

/data 配下に 1.2 TB の領域が確認できますね。

takakuni@ sagemaker_hyperpod_lustre_eks % kubectl exec -it fsx-app -- /bin/bash
[root@fsx-app /]# df -h
Filesystem                Size  Used Avail Use% Mounted on
overlay                   500G  6.8G  494G   2% /
tmpfs                      64M     0   64M   0% /dev
tmpfs                     7.8G     0  7.8G   0% /sys/fs/cgroup
10.0.1.159@tcp:/jm2njbev  1.2T  7.5M  1.2T   1% /data
/dev/nvme0n1p1            100G   20G   81G  20% /etc/hosts
/dev/nvme1n1              500G  6.8G  494G   2% /etc/hostname
shm                        64M     0   64M   0% /dev/shm
tmpfs                      15G   12K   15G   1% /run/secrets/kubernetes.io/serviceaccount
tmpfs                     7.8G     0  7.8G   0% /proc/acpi
tmpfs                     7.8G     0  7.8G   0% /sys/firmware

また、サンプルファイルとしてアップロードしたファイルの中身も確認できました。

[root@fsx-app /]# cat /data/
hello_from_s3.txt  out.txt
[root@fsx-app /]# cat /data/hello_from_s3.txt
hello! from S3![root@fsx-app /]#

S3 側にも Pod によって作成されたデータが格納されていました。

2025-01-05 at 10.26.51-hyprpd-eks-hyperpod-data-120569620949 - S3 バケット  S3  us-west-2@2x.png

まとめ

以上、「EKS オーケストレータを使った SageMaker HyperPod クラスターで FSx for Lustre をマウントしてみた」でした。

ほとんど「Amazon FSx for Lustre CSI Driver 使ってみた」な内容でしたが、参考になれば幸いです。

クラウド事業本部コンサルティング部のたかくに(@takakuni_)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.